ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / core / network / src / commonMain / kotlin / org / meshtastic / core / network / radio / BleRadioInterface.kt
Displaying Raw β’ Download
core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt a3c0a4832d139b9ed15cc2fdd22d9d2c0bef1db4 (a3c0a483) Text, 23.92 KB
T8b949e/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72package T7ee787org.meshtastic.core.network.radio
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CoroutineExceptionHandler
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.DelicateCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.GlobalScope
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.coroutineScope
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.isActive
Tff7b72import T7ee787kotlinx.coroutines.job
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnection
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanner
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.ble.BluetoothRepository
Tff7b72import T7ee787org.meshtastic.core.ble.DisconnectReason
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
Tff7b72import T7ee787org.meshtastic.core.ble.classifyBleException
Tff7b72import T7ee787org.meshtastic.core.ble.retryBleOperation
Tff7b72import T7ee787org.meshtastic.core.ble.toMeshtasticRadioProfile
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.model.RadioNotConnectedException
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransport
Tff7b72import T7ee787org.meshtastic.proto.Heartbeat
Tff7b72import T7ee787org.meshtastic.proto.ToRadio
Tff7b72import T7ee787kotlin.concurrent.Volatile
Tff7b72import T7ee787kotlin.concurrent.atomics.AtomicInt
Tff7b72import T7ee787kotlin.concurrent.atomics.ExperimentalAtomicApi
Tff7b72import T7ee787kotlin.time.Duration
Tff7b72import T7ee787kotlin.time.Duration.Companion.milliseconds
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3SCAN_RETRY_COUNT Tff7b72= T79c0ff3
Tff7b72private Tff7b72val Te6edf3SCAN_RETRY_DELAY Tff7b72= T79c0ff1.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3CONNECTION_TIMEOUT Tff7b72= T79c0ff1T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_FAILURE_THRESHOLD Tff7b72= T79c0ff3
Tff7b72private Tff7b72val Te6edf3RECONNECT_BASE_DELAY Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3RECONNECT_MAX_DELAY Tff7b72= T79c0ff6T79c0ff0.Te6edf3seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3RECONNECT_MAX_FAILURES Tff7b72= T79c0ff1T79c0ff0
T8b949e/** Settle delay before each connection attempt to let the Android BLE stack finish any pending disconnect cleanup. */
Tff7b72private Tff7b72val Te6edf3SETTLE_DELAY Tff7b72= T79c0ff1.Te6edf3seconds
T8b949e/**
* Minimum time a BLE connection must stay up before we consider it "stable" and reset
* [BleRadioInterface.consecutiveFailures]. Without this, a device at the edge of BLE range can repeatedly connect for a
* fraction of a second and drop β each brief connection resets the failure counter so [RECONNECT_FAILURE_THRESHOLD] is
* never reached, and the app never signals [ConnectionState.DeviceSleep].
*
* The value (5 s) is long enough that only connections that survive past the initial GATT setup are treated as genuine,
* but short enough that normal reconnects after light-sleep still reset the counter promptly.
*/
Tff7b72private Tff7b72val Te6edf3MIN_STABLE_CONNECTION Tff7b72= T79c0ff5.Te6edf3seconds
T8b949e/**
* Returns the reconnect backoff delay for a given consecutive failure count.
*
* Backoff schedule: 1 failure β 5 s 2 failures β 10 s 3 failures β 20 s 4 failures β 40 s 5+ failures β 60 s (capped)
*/
Tff7b72internal Tff7b72fun Td2a8ffcomputeReconnectBackoffTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3Duration Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72<Tff7b72= T79c0ff0Tb4b4b4) Tff7b72return Te6edf3RECONNECT_BASE_DELAY
Tff7b72val Te6edf3multiplier Tff7b72= T79c0ff1 Te6edf3shl Tb4b4b4(Te6edf3consecutiveFailures Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3coerceAtMostTb4b4b4(T79c0ff4Tb4b4b4)
Tff7b72return Te6edf3minOfTb4b4b4(Te6edf3RECONNECT_BASE_DELAY Tff7b72* Te6edf3multiplierTb4b4b4, Te6edf3RECONNECT_MAX_DELAYTb4b4b4)
Tb4b4b4}
T8b949e/**
* Delay after writing a heartbeat before re-polling FROMRADIO.
*
* The ESP32 firmware processes TORADIO writes asynchronously (NimBLE callback β FreeRTOS main task queue β
* `handleToRadio()` β `heartbeatReceived = true`). The immediate drain trigger in
* [KableMeshtasticRadioProfile.sendToRadio] fires before this completes, so the `queueStatus` response is not yet
* available. 200 ms is well above observed ESP32 task scheduling latency (~10β50 ms) while remaining imperceptible to
* the user.
*/
Tff7b72private Tff7b72val Te6edf3HEARTBEAT_DRAIN_DELAY Tff7b72= T79c0ff2T79c0ff0T79c0ff0.Te6edf3milliseconds
Tff7b72private Tff7b72val Te6edf3SCAN_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3GATT_CLEANUP_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
T8b949e/**
* A [RadioTransport] implementation for BLE devices using the common BLE abstractions (which are powered by Kable).
*
* This class handles the high-level connection lifecycle for Meshtastic radios over BLE, including:
* - Bonding and discovery.
* - Automatic reconnection logic.
* - MTU and connection parameter monitoring.
* - Routing raw byte packets between the radio and [RadioInterfaceService].
*
* @param serviceScope The coroutine scope to use for launching coroutines.
* @param scanner The BLE scanner.
* @param bluetoothRepository The Bluetooth repository.
* @param connectionFactory The BLE connection factory.
* @param service The [RadioInterfaceService] to use for handling radio events.
* @param address The BLE address of the device to connect to.
*/
Tff7b72class T56d364BleRadioInterfaceTb4b4b4(
Tff7b72private Tff7b72val Te6edf3serviceScopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Tff7b72private Tff7b72val Te6edf3scannerTb4b4b4: Te6edf3BleScannerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3bluetoothRepositoryTb4b4b4: Te6edf3BluetoothRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3connectionFactoryTb4b4b4: Te6edf3BleConnectionFactoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3serviceTb4b4b4: Te6edf3RadioInterfaceServiceTb4b4b4,
Tff7b72internal Tff7b72val Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3RadioTransport Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3exceptionHandler Tff7b72= Te6edf3CoroutineExceptionHandler Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3throwable Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3throwableTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Uncaught exception in connectionScopeTa5d6ff" Tb4b4b4}
Te6edf3serviceScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in exception handlerTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3connectionScopeTb4b4b4: Te6edf3CoroutineScope Tff7b72=
Te6edf3CoroutineScopeTb4b4b4(
Te6edf3serviceScopeTb4b4b4.Te6edf3coroutineContext Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Te6edf3serviceScopeTb4b4b4.Te6edf3coroutineContextTb4b4b4.Te6edf3jobTb4b4b4) Tff7b72+ Te6edf3exceptionHandlerTb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3bleConnectionTb4b4b4: Te6edf3BleConnection Tff7b72= Te6edf3connectionFactoryTb4b4b4.Te6edf3createTb4b4b4(Te6edf3connectionScopeTb4b4b4, Te6edf3addressTb4b4b4)
Tff7b72private Tff7b72val Te6edf3writeMutexTb4b4b4: Te6edf3Mutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3connectionStartTimeTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3packetsReceivedTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3packetsSentTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3bytesReceivedTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3bytesSentTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3isFullyConnected Tff7b72= Tff7b72false
Tff7b72private Tff7b72var Te6edf3connectionJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalAtomicApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72private Tff7b72val Te6edf3heartbeatNonce Tff7b72= Te6edf3AtomicIntTb4b4b4(T79c0ff0Tb4b4b4)
Tff7b72init Tb4b4b4{
Te6edf3connectTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// --- Connection & Discovery Logic ---
T8b949e/** Robustly finds the device. First checks bonded devices, then performs a short scan if not found. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fffindDeviceTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleDevice Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3stateTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3bondedDevices
Tb4b4b4.Te6edf3firstOrNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{
Tff7b72return Tffa657it
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not found in bonded list, scanningTa5d6ff" Tb4b4b4}
Te6edf3repeatTb4b4b4(Te6edf3SCAN_RETRY_COUNTTb4b4b4) Tb4b4b4{ Te6edf3attempt Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3d Tff7b72=
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3SCAN_TIMEOUTTb4b4b4) Tb4b4b4{
Te6edf3scannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3timeout Tff7b72= Te6edf3SCAN_TIMEOUTTb4b4b4, Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4, Te6edf3address Tff7b72= Te6edf3addressTb4b4b4)Tb4b4b4.Te6edf3first Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3d Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tff7b72return Te6edf3d
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3vTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Scan attempt failed or timed outTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3attempt Tff7b72< Te6edf3SCAN_RETRY_COUNT Tff7b72- T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3SCAN_RETRY_DELAYTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffDevice not found at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3connectionJob Tff7b72=
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72while Tb4b4b4(Te6edf3isActiveTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
T8b949e// Settle delay: let the Android BLE stack finish any pending
T8b949e// disconnect cleanup before starting a new connection attempt.
Te6edf3delayTb4b4b4(Te6edf3SETTLE_DELAYTb4b4b4)
Te6edf3connectionStartTime Tff7b72= Te6edf3nowMillis
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection attempt startedTa5d6ff" Tb4b4b4}
Tff7b72val Te6edf3device Tff7b72= Te6edf3findDeviceTb4b4b4(Tb4b4b4)
T8b949e// Bond before connecting: firmware may require an encrypted link,
T8b949e// and without a bond Android fails with status 5 or 133.
T8b949e// No-op on Desktop/JVM where the OS handles pairing automatically.
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3isBondedTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not bonded, initiating bondingTa5d6ff" Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondTb4b4b4(Te6edf3deviceTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding successfulTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding failed, attempting connection anywayTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72val Te6edf3state Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3CONNECTION_TIMEOUTTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3state Tff7b72!is Te6edf3BleConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to connect to device at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e// Only reset failures if connection was stable (see MIN_STABLE_CONNECTION).
Tff7b72val Te6edf3gattConnectedAt Tff7b72= Te6edf3nowMillis
Te6edf3isFullyConnected Tff7b72= Tff7b72true
Te6edf3onConnectedTb4b4b4(Tb4b4b4)
T8b949e// Scope the connectionState listener to this iteration so it's
T8b949e// cancelled automatically before the next reconnect cycle.
Tff7b72var Te6edf3disconnectReasonTb4b4b4: Te6edf3DisconnectReason Tff7b72= Te6edf3DisconnectReasonTb4b4b4.Te6edf3Unknown
Te6edf3coroutineScope Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionState
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3s Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3s Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Disconnected Tff7b72&Tff7b72& Te6edf3isFullyConnectedTb4b4b4) Tb4b4b4{
Te6edf3isFullyConnected Tff7b72= Tff7b72false
Te6edf3disconnectReason Tff7b72= Te6edf3sTb4b4b4.Te6edf3reason
Te6edf3onDisconnectedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72> Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] bleConnection.connectionState flow crashedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3discoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4)
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3first Tb4b4b4{ Tffa657it Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Disconnected Tb4b4b4}
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection dropped (reason: Tffd700$Te6edf3disconnectReasonTa5d6ff), preparing to reconnectTa5d6ff"
Tb4b4b4}
T8b949e// Skip failure counting for intentional disconnects.
Tff7b72if Tb4b4b4(Te6edf3disconnectReason Tff7b72is Te6edf3DisconnectReasonTb4b4b4.Te6edf3LocalDisconnectTb4b4b4) Tb4b4b4{
Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Tff7b72continue
Tb4b4b4}
T8b949e// A connection that drops almost immediately (< MIN_STABLE_CONNECTION)
T8b949e// is treated as a failure β the BLE stack may have "connected" to a
T8b949e// cached GATT profile before realising the device is gone.
Tff7b72val Te6edf3connectionUptime Tff7b72= Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3gattConnectedAtTb4b4b4)Tb4b4b4.Te6edf3milliseconds
Tff7b72if Tb4b4b4(Te6edf3connectionUptime Tff7b72>Tff7b72= Te6edf3MIN_STABLE_CONNECTIONTb4b4b4) Tb4b4b4{
Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection lasted only Tffd700$Te6edf3connectionUptimeTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(< Tffd700$Te6edf3MIN_STABLE_CONNECTIONTa5d6ff) β treating as failure Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff"
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_MAX_FAILURESTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Giving up after Tffd700$Te6edf3consecutiveFailuresTa5d6ff unstable connectionsTa5d6ff" Tb4b4b4}
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(
Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4,
Te6edf3errorMessage Tff7b72= Ta5d6ff"Ta5d6ffDevice unreachable (unstable connection)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_FAILURE_THRESHOLDTb4b4b4) Tb4b4b4{
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(
Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3errorMessage Tff7b72= Ta5d6ff"Ta5d6ffDevice unreachable (unstable connection)Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection coroutine cancelledTa5d6ff" Tb4b4b4}
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3failureTime Tff7b72= Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTimeTb4b4b4)Tb4b4b4.Te6edf3milliseconds
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to connect to device after Tffd700$Te6edf3failureTimeTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff"
Tb4b4b4}
T8b949e// Give up permanently to stop draining battery.
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_MAX_FAILURESTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Giving up after Tffd700$Te6edf3consecutiveFailuresTa5d6ff consecutive failuresTa5d6ff" Tb4b4b4}
Tff7b72val Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3eTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tff7b72returnTf0883e@launch
Tb4b4b4}
T8b949e// Signal DeviceSleep so MeshConnectionManagerImpl starts its sleep timeout.
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3RECONNECT_FAILURE_THRESHOLDTb4b4b4) Tb4b4b4{
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3backoff Tff7b72= Te6edf3computeReconnectBackoffTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Retrying in Tffd700$Te6edf3backoffTa5d6ff (failure #Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3delayTb4b4b4(Te6edf3backoffTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffonConnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3deviceFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3device Tff7b72-Tff7b72>
Tff7b72val Te6edf3rssi Tff7b72= Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3deviceTb4b4b4.Te6edf3readRssiTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection confirmed. Initial RSSI: Tffd700$Te6edf3rssiTa5d6ff dBmTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to read initial connection RSSITa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffonDisconnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioService Tff7b72= Tff7b72null
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE disconnected - Tffd700${Te6edf3formatSessionStatsTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
T8b949e// Signal immediately so the UI reflects the disconnect while reconnect continues.
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffdiscoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3radioService Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3toMeshtasticRadioProfileTb4b4b4(Tb4b4b4)
Te6edf3radioServiceTb4b4b4.Te6edf3fromRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet fromRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in fromRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3radioServiceTb4b4b4.Te6edf3logRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet logRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in logRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72thisTf0883e@BleRadioInterface.radioService Tff7b72= Te6edf3radioService
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service active and characteristics subscribedTa5d6ff" Tb4b4b4}
T8b949e// Wait for FROMNUM CCCD write before triggering the Meshtastic handshake.
Te6edf3radioServiceTb4b4b4.Te6edf3awaitSubscriptionReadyTb4b4b4(Tb4b4b4)
T8b949e// Log negotiated MTU for diagnostics
Tff7b72val Te6edf3maxLen Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3maximumWriteValueLengthTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): Tffd700$Te6edf3maxLenTa5d6ff bytesTa5d6ff" Tb4b4b4}
Tff7b72thisTf0883e@BleRadioInterface.service.onConnectTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service discovery or operation failedTa5d6ff" Tb4b4b4}
T8b949e// Disconnect to let the outer reconnect loop see a clean Disconnected state.
T8b949e// Do NOT call handleFailure here β the reconnect loop owns failure counting.
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3ignoredTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3ignoredTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] disconnect() failed after profile errorTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3radioServiceTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3MeshtasticRadioProfileTff7b72? Tff7b72= Tff7b72null
T8b949e// --- RadioTransport Implementation ---
T8b949e/**
* Sends a packet to the radio with retry support.
*
* @param p The packet to send.
*/
Tff7b72override Tff7b72fun Td2a8ffhandleSendToRadioTb4b4b4(Te6edf3pTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3currentService Tff7b72= Te6edf3radioService
Tff7b72if Tb4b4b4(Te6edf3currentService Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3writeMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3currentServiceTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3pTb4b4b4) Tb4b4b4}
Te6edf3packetsSentTff7b72+Tff7b72+
Te6edf3bytesSent Tff7b72+Tff7b72= Te6edf3pTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Wrote packet #Tffd700$Te6edf3packetsSentTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffto toRadio (Tffd700${Te6edf3pTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total TX: Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to write packet to toRadioCharacteristic after Ta5d6ff" Tff7b72+
Ta5d6ff"Tffd700$Te6edf3packetsSentTa5d6ff successful writesTa5d6ff"
Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] toRadio characteristic unavailable, can't send dataTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalAtomicApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffkeepAliveTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Send a ToRadio heartbeat so the firmware resets its power-saving idle timer.
T8b949e// The firmware only resets the timer on writes to the TORADIO characteristic; a
T8b949e// BLE-level GATT keepalive is invisible to it. Without this the device may enter
T8b949e// light-sleep and drop the BLE connection after ~60 s of application inactivity.
T8b949e//
T8b949e// Each heartbeat uses a distinct nonce to vary the wire bytes, preventing the
T8b949e// firmware's per-connection duplicate-write filter from silently dropping it.
Tff7b72val Te6edf3nonce Tff7b72= Te6edf3heartbeatNonceTb4b4b4.Te6edf3fetchAndAddTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE keepAlive β sending ToRadio heartbeat (nonce=Tffd700$Te6edf3nonceTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3handleSendToRadioTb4b4b4(Te6edf3ToRadioTb4b4b4(Te6edf3heartbeat Tff7b72= Te6edf3HeartbeatTb4b4b4(Te6edf3nonce Tff7b72= Te6edf3nonceTb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e// The firmware responds to heartbeats by queuing a `queueStatus` FromRadio packet
T8b949e// on the next getFromRadio() call, but it does NOT send a FROMNUM notification for
T8b949e// it. The immediate drain trigger in sendToRadio() fires before the ESP32's async
T8b949e// task queue has processed the heartbeat, so the response sits unread. Schedule a
T8b949e// delayed re-drain to pick it up.
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3HEARTBEAT_DRAIN_DELAYTb4b4b4)
Te6edf3radioServiceTff7b72?.Te6edf3requestDrainTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Closes the connection to the device. */
Tff7b72override Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Disconnecting. Tffd700${Te6edf3formatSessionStatsTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Te6edf3connectionScopeTb4b4b4.Te6edf3cancelTb4b4b4(Ta5d6ff"Ta5d6ffclose() calledTa5d6ff"Tb4b4b4)
T8b949e// GATT cleanup must outlive serviceScope cancellation β GlobalScope is intentional.
T8b949e// SharedRadioInterfaceService cancels serviceScope immediately after close(), so a
T8b949e// coroutine launched there may never run, leaking BluetoothGatt (causes GATT 133).
Tf0883e@OptInTb4b4b4(Te6edf3DelicateCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Te6edf3GlobalScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3GATT_CLEANUP_TIMEOUTTb4b4b4) Tb4b4b4{ Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in close()Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffdispatchPacketTb4b4b4(Te6edf3packetTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Te6edf3packetsReceivedTff7b72+Tff7b72+
Te6edf3bytesReceived Tff7b72+Tff7b72= Te6edf3packetTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Dispatching packet #Tffd700$Te6edf3packetsReceivedTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total RX: Tffd700$Te6edf3bytesReceivedTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Te6edf3serviceTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffhandleFailureTb4b4b4(Te6edf3throwableTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}
T8b949e/** Formats a one-line session statistics summary for logging. */
Tff7b72private Tff7b72fun Td2a8ffformatSessionStatsTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String Tb4b4b4{
Tff7b72val Te6edf3uptime Tff7b72= Tff7b72if Tb4b4b4(Te6edf3connectionStartTime Tff7b72> T79c0ff0Tb4b4b4) Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTime Tff7b72else T79c0ff0
Tff7b72return Ta5d6ff"Ta5d6ffUptime: Tffd700${Te6edf3uptimeTffd700}Ta5d6ffms, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets RX: Tffd700$Te6edf3packetsReceivedTa5d6ff (Tffd700$Te6edf3bytesReceivedTa5d6ff bytes), Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets TX: Tffd700$Te6edf3packetsSentTa5d6ff (Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8fftoDisconnectReasonTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3PairTff7b72<Tffa657BooleanTb4b4b4, Tffa657StringTff7b72> Tb4b4b4{
Te6edf3classifyBleExceptionTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{
Tff7b72return Tffa657itTb4b4b4.Te6edf3isPermanent Te6edf3to Tffa657itTb4b4b4.Te6edf3message
Tb4b4b4}
Tff7b72val Te6edf3msg Tff7b72=
Tff7b72when Tb4b4b4(Tff7b72thisTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3RadioNotConnectedException Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Ta5d6ff"Ta5d6ffDevice not foundTa5d6ff"
Tff7b72is Te6edf3NoSuchElementExceptionTb4b4b4,
Tff7b72is Te6edf3IllegalArgumentExceptionTb4b4b4,
Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffRequired characteristic missingTa5d6ff"
Tff7b72else Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72?: Ta5d6ff"Ta5d6ffUnknownTa5d6ff"
Tb4b4b4}
Tff7b72return Tff7b72false Te6edf3to Te6edf3msg
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.2 - Generated in 0.07s